Completed
Push — master ( 50381b...d599db )
by Jeff
04:14
created

Preload.preload   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 0
1
/** global: updateScreenUrl */
2
3
/**
4
 * Screen class constructor
5
 * @param {string} updateScreenUrl global screen update checks url
6
 */
7
function Screen(updateScreenUrl) {
8
  this.fields = [];
9
  this.url = updateScreenUrl;
10
  this.lastChanges = null;
11
  this.endAt = null;
12
  this.nextUrl = null;
13
  this.cache = new Preload();
14
  this.debugMode = false;
15
}
16
17
/**
18
 * Ajax GET on updateScreenUrl to check lastChanges timestamp and reload if necessary
19
 */
20
Screen.prototype.checkUpdates = function() {
21
  var s = this;
22
  $.get(this.url, function(j) {
23
    if (j.success) {
24
      if (s.lastChanges == null) {
25
        s.lastChanges = j.data.lastChanges;
26
      } else if (s.lastChanges != j.data.lastChanges) {
27
        // Remote screen updated, we should reload
28
        s.reloadIn(0);
29
        s.nextUrl = null;
30
        return;
31
      }
32
33
      if (j.data.duration > 0) {
34
        // Setup next screen
35
        s.reloadIn(j.data.duration * 1000);
36
        s.nextUrl = j.data.nextScreenUrl;
37
      }
38
    } else if (j.message == 'Unauthorized') {
39
      screen.reloadIn(0);
40
    }
41
  });
42
}
43
44
/**
45
 * Start Screen reload procedure, checking for every field timeout
46
 */
47
Screen.prototype.reloadIn = function(minDuration) {
48
  var endAt = Date.now() + minDuration;
49
  if (this.endAt != null && this.endAt < endAt) {
50
    return;
51
  }
52
53
  if (this.cache.hasPreloadingContent(true)) {
54
    // Do not break preloading
55
    return;
56
  }
57
58
  this.endAt = Date.now() + minDuration;
59
  for (var i in this.fields) {
60
    if (!this.fields.hasOwnProperty(i)) {
61
      continue;
62
    }
63
    var f = this.fields[i];
64
    if (f.timeout && f.endAt > this.endAt) {
65
      // Always wait for content display end
66
      this.endAt = f.endAt;
67
    }
68
  }
69
70
  if (Date.now() >= this.endAt) {
71
    this.reloadNow();
72
  }
73
}
74
75
/**
76
 * Actual Screen reload action
77
 */
78
Screen.prototype.reloadNow = function() {
79
  if (this.nextUrl) {
80
    window.location = this.nextUrl;
81
  } else {
82
    window.location.reload();
83
  }
84
}
85
86
/**
87
 * Check every field for content
88
 * @param  {Content} data 
89
 * @return {boolean} content is displayed
90
 */
91
Screen.prototype.displaysData = function(data) {
92
  return this.fields.filter(function(field) {
93
    return field.current && field.current.data == data;
94
  }).length > 0;
95
}
96
97
Screen.prototype.debug = function() {
98
  if (!this.debugMode) {
99
    return;
100
  }
101
  var d = $('#debug').html();
102
  d += Array.prototype.slice.call(arguments) + '<br />';
103
  $('#debug').html(d);
104
}
105
106
107
/**
108
 * Content class constructor
109
 * @param {array} c content attributes
110
 */
111
function Content(c) {
112
  this.id = c.id;
113
  this.data = c.data;
114
  this.duration = c.duration * 1000;
115
  this.type = c.type;
116
  this.src = null;
117
118
  if (this.shouldPreload()) {
119
    this.queuePreload();
120
  }
121
}
122
123
/**
124
 * Check if content should be ajax preloaded
125
 * @return {boolean}
126
 */
127
Content.prototype.shouldPreload = function() {
128
  return this.canPreload() && !this.isPreloadingOrQueued() && !this.isPreloaded();
129
}
130
131
/**
132
 * Check if content has pre-loadable material
133
 * @return {boolean} 
134
 */
135
Content.prototype.canPreload = function() {
136
  return this.getResource() && this.type.search(/Video|Image|Agenda/) != -1;
137
}
138
139
/**
140
 * Check if content is displayable (preloaded and not too long)
141
 * @return {Boolean} can display
142
 */
143
Content.prototype.canDisplay = function() {
144
  return (screen.endAt == null || Date.now() + this.duration < screen.endAt) && this.isPreloaded();
145
}
146
147
/**
148
 * Extract url from contant data
149
 * @return {string} resource url
150
 */
151
Content.prototype.getResource = function() {
152
  if (this.src) {
153
    return this.src;
154
  }
155
  var srcMatch = this.data.match(/src="([^"]+)"/);
156
  if (!srcMatch) {
157
    return false;
158
  }
159
  var src = srcMatch[1];
160
  if (src.indexOf('/') === 0) {
161
    src = window.location.origin + src;
162
  }
163
  if (src.indexOf('http') !== 0) {
164
    return false;
165
  }
166
  src = src.replace(/#.*/g, '');
167
168
  this.src = src;
169
  return src;
170
}
171
172
/** Set content cache status
173
 * @param {string} expires header
174
 */
175
Content.prototype.setPreloadState = function(expires) {
176
  screen.cache.setState(this.getResource(), expires);
177
}
178
179
/**
180
 * Check cache for preload status of content
181
 * @return {Boolean} 
182
 */
183
Content.prototype.isPreloaded = function() {
184
  if (!this.canPreload()) {
185
    return true;
186
  }
187
188
  return screen.cache.isPreloaded(this.getResource());
189
}
190
191
/**
192
 * Check cache for in progress or future preloading
193
 * @return {Boolean} is preloading
194
 */
195
Content.prototype.isPreloadingOrQueued = function() {
196
  return this.isPreloading() || this.isInPreloadQueue();
197
}
198
199
/**
200
 * Check cache for in progress preloading
201
 * @return {Boolean} is preloading
202
 */
203
Content.prototype.isPreloading = function() {
204
  return screen.cache.isPreloading(this.getResource());
205
}
206
207
/**
208
 * Check cache for queued preloading
209
 * @return {Boolean} is in preload queue
210
 */
211
Content.prototype.isInPreloadQueue = function() {
212
  return screen.cache.isInPreloadQueue(this.getResource());
213
}
214
215
/**
216
 * Ajax call to preload content
217
 */
218
Content.prototype.preload = function() {
219
  var src = this.getResource();
220
  if (!src) {
221
    return;
222
  }
223
224
  screen.cache.preload(src);
225
}
226
227
/**
228
 * Preload content or add to preload queue
229
 */
230
Content.prototype.queuePreload = function() {
231
  var src = this.getResource();
232
  if (!src) {
233
    return;
234
  }
235
236
  if (screen.cache.hasPreloadingContent(false)) {
237
    screen.debug('queue', this.src);
238
    this.setPreloadState(Preload.state.PRELOADING_QUEUE);
239
  } else {
240
    this.preload();
241
  }
242
}
243
244
245
/**
246
 * Preload class constructor
247
 * Mostly used to store constants
248
 */
249
function Preload() {
250
  this.cache = {};
251
}
252
253
Preload.prototype.setState = function(res, expires) {
254
  if (expires === null || expires == '') {
255
    expires = Preload.state.NO_EXPIRE_HEADER;
256
  }
257
258
  this.cache[res] = expires < -1 ? expires : Preload.state.OK
259
}
260
261
Preload.prototype.isPreloaded = function(res) {
262
  var state = this.cache[res];
263
264
  return state === Preload.state.OK || state === Preload.state.NO_EXPIRE_HEADER;
265
}
266
267
Preload.prototype.isPreloading = function(res) {
268
  return this.cache[res] === Preload.state.PRELOADING;
269
}
270
271
Preload.prototype.isInPreloadQueue = function(res) {
272
  return this.cache[res] === Preload.state.PRELOADING_QUEUE;
273
}
274
275
Preload.prototype.hasPreloadingContent = function(withQueue) {
276
  for (var res in this.cache) {
277
    if (!this.cache.hasOwnProperty(res)) {
278
      continue;
279
    }
280
281
    if (this.isPreloading(res) || (withQueue && this.isInPreloadQueue(res))) {
282
      return true;
283
    }
284
  }
285
286
  return false;
287
}
288
289
Preload.prototype.preload = function(res) {
290
  screen.debug('preloading', res)
291
  screen.cache.setState(res, Preload.state.PRELOADING);
292
293
  $.ajax(res).done(function(data, textStatus, jqXHR) {
294
    screen.cache.setState(res, jqXHR.getResponseHeader('Expires'));
295
    screen.debug('preloaded', res);
296
  }).fail(function() {
297
    screen.cache.setPreloadState(res, Preload.state.HTTP_FAIL);
298
    screen.debug('failed', res);
299
  }).always(function() {
300
    var res = screen.cache.next();
301
    if (res) {
302
      screen.debug('next', res);
303
      screen.cache.preload(res);
304
    }
305
  });
306
}
307
308
Preload.prototype.next = function() {
309
  for (var res in this.cache) {
310
    if (!this.cache.hasOwnProperty(res)) {
311
      continue;
312
    }
313
314
    if (this.isInPreloadQueue(res)) {
315
      return res;
316
    }
317
  }
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
318
}
319
320
/**
321
 * Preload states
322
 */
323
Preload.state = {
324
  PRELOADING: -2,
325
  PRELOADING_QUEUE: -3,
326
  HTTP_FAIL: -4,
327
  NO_EXPIRE_HEADER: -5,
328
  OK: -6,
329
}
330
331
332
/**
333
 * Field class constructor
334
 * @param {jQuery.Object} $f field object
335
 */
336
function Field($f) {
337
  this.$field = $f;
338
  this.id = $f.attr('data-id');
339
  this.url = $f.attr('data-url');
340
  this.types = $f.attr('data-types').split(' ');
341
  this.canUpdate = this.url != null;
342
  this.contents = [];
343
  this.previous = null;
344
  this.current = null;
345
  this.next = null;
346
  this.timeout = null;
347
  this.endAt = null;
348
}
349
350
/**
351
 * Retrieves contents from backend for this field
352
 */
353
Field.prototype.fetchContents = function() {
354
  if (!this.canUpdate) {
355
    return;
356
  }
357
358
  var f = this;
359
  $.get(this.url, function(j) {
360
    if (j.success) {
361
      f.contents = j.next.map(function(c) {
362
        return new Content(c);
363
      });
364
      if (!f.timeout && f.contents.length) {
365
        f.pickNext();
366
      }
367
    } else {
368
      f.setError(j.message || 'Error');
369
    }
370
  });
371
}
372
373
/**
374
 * Display error in field text
375
 */
376
Field.prototype.setError = function(err) {
377
  this.display(err);
378
}
379
380
/**
381
 * Randomize order
382
 */
383
Field.prototype.randomizeSortContents = function() {
384
  this.contents = this.contents.sort(function() {
385
    return Math.random() - 0.5;
386
  });
387
}
388
389
/**
390
 * Loop through field contents to pick next displayable content
391
 */
392
Field.prototype.pickNext = function() {
393
  if (screen.endAt != null && Date.now() >= screen.endAt) { // Stoping screen
394
    screen.reloadNow();
395
    return;
396
  }
397
398
  var f = this;
399
  this.previous = this.current;
400
  this.current = null;
401
  var pData = this.previous && this.previous.data;
402
  // Avoid repeat & other field same content
403
  this.randomizeSortContents();
404
  for (var i = 0; i < this.contents.length; i++) {
405
    var c = this.contents[i];
406
    // Skip too long or not preloaded content 
407
    if (!c.canDisplay()) {
408
      continue;
409
    }
410
411
    if (c.data == pData) {
412
      // Will repeat, avoid if enough content
413
      if (this.contents.length < 2) {
414
        this.next = c;
415
        break;
416
      }
417
      continue;
418
    }
419
420
    if (screen.displaysData(c.data)) {
421
      // Same content already displayed on other field, avoid if enough content
422
      if (this.contents.length < 3) {
423
        this.next = c;
424
        break;
425
      }
426
      continue;
427
    }
428
429
    this.next = c;
430
    break
431
  }
432
433
  if (this.next) {
434
    this.displayNext();
435
  } else {
436
    setTimeout(function() {
437
      f.pickNext();
438
    }, 600);
439
  }
440
}
441
442
/**
443
 * Setup next content for field and display it
444
 */
445
Field.prototype.displayNext = function() {
446
  var f = this;
447
  if (this.next && this.next.duration > 0) {
448
    this.current = this.next
449
    this.next = null;
450
    this.display(this.current.data);
451
    if (this.timeout) {
452
      clearTimeout(this.timeout);
453
    }
454
    this.endAt = Date.now() + this.current.duration;
455
    this.timeout = setTimeout(function() {
456
      f.pickNext();
457
    }, this.current.duration);
458
  }
459
}
460
461
/**
462
 * Display data in field HTML
463
 * @param  {string} data 
464
 */
465
Field.prototype.display = function(data) {
466
  if (screen.debugMode) {
467
    return;
468
  }
469
  this.$field.html(data);
470
  this.$field.show();
471
  if (this.$field.text() != '') {
472
    this.$field.textfill({
473
      maxFontPixels: 0,
474
    });
475
  }
476
}
477
478
// Global screen instance
479
var screen = null;
480
481
/**
482
 * jQuery.load event
483
 * Initialize Screen and Fields
484
 * Setup updates interval timeouts
485
 */
486
function onLoad() {
487
  screen = new Screen(updateScreenUrl);
488
  // Init
489
  $('.field').each(function() {
490
    var f = new Field($(this));
491
    f.fetchContents();
492
    screen.fields.push(f);
493
  });
494
495
  if (screen.url) {
496
    // Setup content updates loop
497
    setInterval(function() {
498
      for (var f in screen.fields) {
499
        if (screen.fields.hasOwnProperty(f)) {
500
          screen.fields[f].fetchContents();
501
        }
502
      }
503
      screen.checkUpdates();
504
    }, 30000);
505
    screen.checkUpdates();
506
  }
507
}
508
509
// Run
510
$(onLoad);
511